<?php
/**
 * Created by PhpStorm.
 * User: Qch
 * Date: 2016/8/13
 * Time: 17:24
 */

namespace J;


/**
 *
 * This file is part of the JFramework project for PHP.
 *
 * 本文件修改自修改自AURA
 *
 * @license http://opensource.org/licenses/bsd-license.php BSD
 *
 * 标准自动加载器 [PSR-4]
 *
 *
 */
class Loader
{
    const FILE_NAME_EMPTY   = 0;
    const CLASS_NAME_ERROR  = 1;
    const CLASS_NOT_DEFINED = 2;
    const CLASS_RELOAD      = 3;
    const FILE_NOT_FINED    = 4;

    static protected $message = [
        self::FILE_NAME_EMPTY   => '文件名为空',
        self::CLASS_NAME_ERROR => '{class}名称错误',
        self::CLASS_NOT_DEFINED => '{file}中${class}未定义',
        self::CLASS_RELOAD => '{class}重复加载',
        self::FILE_NOT_FINED => '{class}未找到',
    ];
    /**
     * 已经加载的类，缓存数据
     * 类名到文件的映射
     *
     * @var array
     *
     */
    protected $loaded_class = [];

    /**
     * 域名空间到文件目录的映射
     *
     * @var array
     *
     */
    protected $namespace_paths = [];

    /**
     * 类名到文件的映射，预定义数据
     *
     * @var array
     *
     */
    protected $class_files = [];

    /**
     * 查找类定义文件过程日志，临时数据
     *
     * @var array
     *
     */
    protected $tried_files = [];

    /**
     * @var \Closure
     * function (string $error)
     */
    protected $error_handler = null;

    /**
     * Registers this autoloader with SPL.
     *
     * @param bool $prepend True to prepend to the autoload stack.
     *
     * @return void
     *
     */
    public function register($prepend = false)
    {
        spl_autoload_register([$this, 'load'], true, (bool)$prepend);
    }

    /**
     * Unregisters this autoloader from SPL.
     *
     * @return void
     *
     */
    public function unregister()
    {
        spl_autoload_unregister([$this, 'load']);
    }

    /**
     * 本地化错误信息
     * @param string[] $message 错误信息
     */
    public static function setLocale($message)
    {
        self::$message = $message;
    }

    /**
     * Adds a directory path for a class name prefix.
     *
     * @param string $prefix The class name prefix, e.g. 'J'
     *
     * @param string $paths The directory path leading to the classes
     * with that prefix, e.g. `'/path/to/system/package/J.Framework-dev/src'`.
     *
     * @return void
     *
     */
    public function addNamespace($prefix, $paths)
    {
        if(isset($this->namespace_paths[$prefix]))
            $this->namespace_paths[$prefix] = [];
        foreach ((array)$paths as $path) {
            $path = rtrim($path, DIRECTORY_SEPARATOR);
            $this->namespace_paths[$prefix][] = $path;
        }
    }

    /**
     * Returns the list of all class name prefixes and their paths.
     *
     * @return array
     *
     */
    public function getNamespacePaths()
    {
        return $this->namespace_paths;
    }

    /**
     * Sets the exact file path for an exact class name.
     *
     * @param string $class The exact class name.
     *
     * @param string $path The file path to that class.
     *
     * @return void
     *
     */
    public function setClass($class, $path)
    {
        $this->class_files[$class] = $path;
    }

    /**
     * Returns the list of exact class names and their paths.
     *
     * @return array
     *
     */
    public function getClassFiles()
    {
        return $this->class_files;
    }

    /**
     * Sets all file paths for all class names; this overwrites all previous
     * exact mappings.
     *
     * @param array $class_files An array of class-to-file mappings where the key
     * is the class name and the value is the file path.
     *
     * @return void
     *
     */
    public function setClassFiles(array $class_files)
    {
        $this->class_files = $class_files;
    }

    /**
     * Returns the list of classes and interfaces loaded by the autoloader.
     *
     * @return array An array of key-value pairs where the key is the class
     * or interface name and the value is the file name.
     *
     */
    public function getLoadedClass()
    {
        return $this->loaded_class;
    }

    /**
     * Loads a class or interface using the path or namespace,
     * falling back to the include-path if not found.
     *
     * @param string $class The class or interface to load.
     *
     */
    public function load($class)
    {
        if(!is_classname($class)){
            $this->onError(self::CLASS_NAME_ERROR, compact('class'));
            return;
        }
        // is the class already loaded?
        if ($this->isDeclared($class)) {
            $this->onError(self::CLASS_RELOAD, compact('class'));
            return;
        }

        // find the class file
        $file = $this->find($class);
        if (!$file) {
            $this->onError(self::FILE_NOT_FINED, compact('class'));
            return;
        }

        // load the file
        /** @noinspection PhpIncludeInspection */
        require $file;

        // is the class declared now?
        if (!$this->isDeclared($class)) {
            $this->onError(self::CLASS_NOT_DEFINED, compact('class', 'file'));
            return;
        }

        // done!
        $this->loaded_class[$class] = $file;
    }

    /**
     * Tells if a class, interface or trait exists.
     *
     * @param string $spec The class or interface.
     *
     * @return bool
     *
     */
    protected function isDeclared($spec)
    {
        return class_exists($spec, false)
        || interface_exists($spec, false)
        || trait_exists($spec, false);
    }

    /**
     * Finds the path to a class or interface using the namespace and
     * include-path.
     *
     * @param string $spec The class or interface to find.
     *
     * @return string The absolute path to the class or interface.
     *
     */
    public function find($spec)
    {
        $spec = trim($spec, '\\');

        // does the class exist in the explicit class map?
        if (isset($this->class_files[$spec]))
            return $this->class_files[$spec];

        $this->tried_files = [];

        // .. convert class name to file name ...
        // 请不要在代码中修改$ctf的值
        try {
            $ctf = $this->classToFile($spec);
        } catch (\Exception $e) {
            return false;
        }

        // 各种长度的命名空间定义均支持
        foreach ($this->namespace_paths as $prefix => $paths) {

            // get the length of the prefix
            $len = strlen($prefix);

            // does the prefix match?
            if (substr($spec, 0, $len) != $prefix) continue;

            // ... and go through each of the paths for the prefix
            foreach ($paths as $i => $path) {

                // convert the remaining spec to a file name
                $file = $path . DIRECTORY_SEPARATOR . $ctf;

                // track which paths we have tried
                $this->tried_files[] = $file;

                // does it exist?
                if (is_readable($file)) {
                    // found it; retain in class map
                    return $file;
                }
            }
        }

//        try {
//            $obj = new \SplFileObject($ctf, 'r', true);
//            $path = $obj->getRealPath();
//            $this->tried_files[] = $ctf;
//        } catch (\Exception $e) {
//            $path = false;
//        }

        return false;
    }

    /**
     * PSR-0 模式解析文件名.
     *
     * @param string $spec The name of the class or interface to load.
     *
     * @return string The filename version of the class or interface.
     *
     * @throws \Exception
     */
    public function classToFile($spec)
    {
        if (!separate_classname($spec, $class, $namespace))
            throw new \Exception(self::$message[self::FILE_NAME_EMPTY]);

        $namespace = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
        // convert class underscores
        $file = $namespace
            . str_replace('_', DIRECTORY_SEPARATOR, $class)
            . '.php';

        // done!
        return $file;
    }

    protected function onError($error, $context = [])
    {
        $error = self::$message[$error];
        if ($this->error_handler) ($this->error_handler)($error, $context);
        else echo message($error, $context);
    }

    public function setErrorHandler(\Closure $handler)
    {
        $this->error_handler = $handler;
    }
}
